﻿namespace StyleCop.CSharp
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using System.Xml.Linq;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using StyleCop;

    public class AnalysisHelper
    {
        private static Type[] ruleGroupTypes;

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="AnalysisHelper"/> class.
        /// </summary>
        public AnalysisHelper()
        {
        }
        #endregion Constructors

        #region Private Properties
        private static IEnumerable<Type> RuleGroupTypes
        {
            get
            {
                if (AnalysisHelper.ruleGroupTypes == null)
                {
                    AnalysisHelper.ruleGroupTypes =
                        (from type in typeof(JslRefactoringRules.RuleAnalyzer).Assembly.GetTypes()
                         where type.IsSubclassOf(typeof(SourceAnalyzer))
                         select type).ToArray();
                }

                return AnalysisHelper.ruleGroupTypes;
            }
        }
        #endregion Private Properties

        #region Public Methods
        public static Method MethodFromSourceCode(string sourceCode)
        {
            return AnalysisHelper.MethodsFromSourceCode(sourceCode)
                .Single();
        }

        public static List<Method> MethodsFromSourceCode(string sourceCode)
        {
            MethodCollectorRule.EnableRule = true;
            try
            {
                AnalysisHelper.RunStyleCop(null, sourceCode, null, null);

                return MethodCollectorRule.Methods;
            }
            finally
            {
                MethodCollectorRule.EnableRule = false;
            }
        }

        public static Method MethodFromMethodCode(string methodCode)
        {
            string sourceCode = "namespace MyNamespace { public class Class { " + methodCode + " } }";
            return AnalysisHelper.MethodFromSourceCode(sourceCode);
        }

        /// <summary>
        /// Tests the specified rule.
        /// </summary>
        /// <param name="ruleType">Type of the rule.</param>
        /// <param name="ruleName">Name of the rule.</param>
        /// <param name="sourceCode">The source code.</param>
        /// <param name="expectedViolations">The expected violations.</param>
        public static void Test(Type ruleType, string ruleName, string sourceCode, params ViolationInfo[] expectedViolations)
        {
            MethodCollectorRule.EnableRule = false;

            var actualViolations = new List<ViolationInfo>();
            AnalysisHelper.RunStyleCop(
                ruleName,
                sourceCode,
                console =>
                {
                    // Build a list of all the violations that are generated.
                    console.ViolationEncountered += (sender, e) =>
                    {
                        actualViolations.Add(new ViolationInfo(e.LineNumber, e.Message));
                    };
                },
                console =>
                {
                    var comparer = new ViolationInfoEqualityComparer();

                    // Find the expected violations that weren't generated.
                    var missingExpectedViolations = expectedViolations.Except(actualViolations, comparer)
                        .Select(violation =>
                            string.Format(
                                "Missing violation on line {0} with message [{1}].",
                                violation.LineNumber,
                                violation.Message));

                    // Find the generated violation that wasn't expected.
                    var unexpectedViolations = actualViolations.Except(expectedViolations, comparer)
                        .Select(violation =>
                            string.Format(
                                "Found unexpected violation on line {0} with message [{1}].",
                                violation.LineNumber,
                                violation.Message));

                    // Join all the messages together.
                    var messages = missingExpectedViolations.Concat(unexpectedViolations).Aggregate(
                        new StringBuilder(),
                        (errorMessages, message) => errorMessages.AppendLine(message));

                    // If there were any messages than fail.
                    if (messages.Length > 0)
                    {
                        Assert.Fail(messages.ToString());
                    }
                });
        }
        #endregion Public Methods

        #region Private Methods
        /// <summary>
        /// Builds the settings file.
        /// </summary>
        /// <param name="enabledRuleName">Name of the enabled rule.</param>
        /// <param name="testDirectory">The test directory.</param>
        /// <returns>The file name of the settings file.</returns>
        private static string BuildSettingsFile(
            string enabledRuleName,
            string testDirectory)
        {
            var analyzersElement = new XElement("Analyzers");
            foreach (var ruleGroupType in AnalysisHelper.RuleGroupTypes)
            {
                var analyzerElement =
                    new XElement(
                        "Analyzer",
                        new XAttribute("AnalyzerId", ruleGroupType.FullName));

                analyzersElement.Add(analyzerElement);

                // Build the Rules element with a Rule element for each rule. The enabled rule will be the only one set to true.
                var rulesElement = new XElement("Rules");
                foreach (string ruleName in AnalysisHelper.GetRuleNames(ruleGroupType))
                {
                    rulesElement.Add(
                        new XElement(
                            "Rule",
                            new XAttribute("Name", ruleName),
                            new XElement(
                                "RuleSettings",
                                new XElement(
                                    "BooleanProperty",
                                    new XAttribute("Name", "Enabled"),
                                    ruleName == enabledRuleName ? "True" : "False"))));
                }

                analyzerElement.Add(rulesElement);
            }

            XDocument settingsDocument = new XDocument(
                new XElement(
                    "StyleCopSettings",
                    new XAttribute("Version", "105"),
                    analyzersElement));

            string fileName = Path.Combine(testDirectory, "Settings.StyleCop");
            settingsDocument.Save(fileName);

            return fileName;
        }

        private static IEnumerable<string> GetRuleNames(Type analyzerType)
        {
            return
                (from fieldInfo in analyzerType.GetFields(BindingFlags.Static | BindingFlags.Public)
                 where fieldInfo.FieldType == typeof(string)
                 select (string)fieldInfo.GetValue(null)).ToArray();
        }

        private static void RunStyleCop(
            string ruleName,
            string sourceCode,
            Action<StyleCopConsole> beforeRun,
            Action<StyleCopConsole> afterRun)
        {
            // We will use the unit testing shadow directory to put our files in, etc.
            var testDirectory = Path.GetDirectoryName(typeof(AnalysisHelper).Assembly.Location);

            // Create the source code file in the test directory.
            var sourceFileName = Path.Combine(testDirectory, "AnalysisTarget.cs");
            using (var writer = File.CreateText(sourceFileName))
            {
                writer.Write(sourceCode);
            }

            // Build the list add-in paths. This is the directory the test assembly is in.
            var addinPaths = new List<string>();
            addinPaths.Add(testDirectory);

            var settingsFileName = AnalysisHelper.BuildSettingsFile(ruleName, testDirectory);

            // Set up the source code analysis for the project.
            var console = new StyleCopConsole(settingsFileName, false, null, addinPaths, false);
            var configuration = new Configuration(new string[] { });
            var project = new CodeProject(sourceFileName.GetHashCode(), sourceFileName, configuration);

            // Add in the single source code file.
            console.Core.Environment.AddSourceCode(project, sourceFileName, null);

            if (beforeRun != null)
            {
                beforeRun(console);
            }

            // Process our single source code file.
            var projects = new CodeProject[] { project };
            console.Start(projects, true);

            if (afterRun != null)
            {
                afterRun(console);
            }
        }

        /*
        <StyleCopSettings Version="105">
          <Analyzers>
            <Analyzer AnalyzerId="StyleCop.CSharp.RefactoringRules">
              <Rules>
                <Rule Name="LocalVariablesShouldBeSetWhereTheyAreDeclared">
                  <RuleSettings>
                    <BooleanProperty Name="Enabled">True</BooleanProperty>
                  </RuleSettings>
                </Rule>
                <Rule Name="LocalVariableAssignmentUnused">
                  <RuleSettings>
                    <BooleanProperty Name="Enabled">False</BooleanProperty>
                  </RuleSettings>
                </Rule>
              </Rules>
            </Analyzer>
            <Analyzer AnalyzerId="StyleCop.CSharp.OrderRules">
              <Rules>
                <Rule Name="MembersMustBeInAlphabeticalOrder">
                  <RuleSettings>
                    <BooleanProperty Name="Enabled">False</BooleanProperty>
                  </RuleSettings>
                </Rule>
              </Rules>
            </Analyzer>
          </Analyzers>
        </StyleCopSettings>
        */
        #endregion Private Methods
    }
}